Completed
Push — master ( dfbaad...096145 )
by Esaú
01:54
created

hierarchy-helper.js ➔ instanceDefinedOrNull   B

Complexity

Conditions 10
Paths 4

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 10
c 4
b 0
f 0
nc 4
dl 0
loc 20
rs 7.2765
nop 8

How to fix   Complexity    Many Parameters   

Complexity

Complex classes like hierarchy-helper.js ➔ instanceDefinedOrNull often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
// spec/helpers/hierarchy-helper.js
2
"use strict";
3
4
// :: DEPENDENCIES
5
6
// load native dependencies
7
const path = require("path");
8
const root = path.dirname(path.dirname(__dirname));
9
10
// global variables
11
const noStr   = [{}, true, false, 42, 3.1416, -42, -3.1416, () => null];
12
const noNmb   = [{}, true, false, '', "qwerty", () => null];
13
const name    = "qwerty";
14
const message = "asdf";
15
16
if (typeof Symbol === "function") {
17
    noStr.push(Symbol("symbol"));
18
    noNmb.push(Symbol("symbol"));
19
}
20
21
module.exports = (hierarchy) => {
22
    // check parameters
23
    if (!Array.isArray(hierarchy)) {
24
        throw new Error("hierarchy must be an array");
25
    }
26
27
    // load dependencies
28
    let deps      = hierarchy.map(value => require(path.join(root, value + ".js")));
29
    let klassName = hierarchy.pop();
30
    let Klass     = deps.pop();
31
    const first   = (hierarchy.length === 0);
32
    const third   = (hierarchy.length >= 3);
33
34
    // :: TESTING
35
36
    // test the last class in the hierarchy tree
37
    describe(klassName, () => {
38
39
        // :: INHERITED PROTOTYPE
40
41
        // all inherit from Object
42
        it("should inherit from 'Object'", () => {
43
            expect(new Klass()).toEqual(jasmine.any(Object));
44
        });
45
46
        // check the hierarchy tree
47
        for (let i = 0; i < hierarchy.length; i += 1) {
48
            it("should inherit from '" + hierarchy[i] + "'", () => {
49
                expect(new Klass()).toEqual(jasmine.any(deps[i]));
50
            });
51
        }
52
53
        // check inherited properties
54
        if (!first) {
55
            it("should have a prototype property named 'name'", () => {
56
                expect(Klass.prototype).toHaveString("name");
57
            });
58
59
            it("should have a prototype property named 'message'", () => {
60
                expect(Klass.prototype).toHaveString("message");
61
            });
62
63
            it("should have a prototype property named 'code'", () => {
64
                expect(Klass.prototype).toHaveMember("code");
65
            });
66
        }
67
68
        // check Object methods
69
        it("should have a prototype method named 'toString()'", () => {
70
            expect(Klass.prototype).toHaveMethod("toString");
71
        });
72
73
        // check inherited methods
74
        if (!first) {
75
            it("should have a prototype method named 'native()'", () => {
76
                expect(Klass.prototype).toHaveMethod("native");
77
            });
78
        }
79
80
        // :: EXTENDED PROTOTYPE
81
82
        // check extended properties
83
        if (first) {
84
            it("should have a prototype property named 'name'", () => {
85
                expect(Klass.prototype).toHaveString("name");
86
            });
87
88
            it("should have a prototype property named 'message'", () => {
89
                expect(Klass.prototype).toHaveString("message");
90
            });
91
92
            it("should have a prototype property named 'code'", () => {
93
                expect(Klass.prototype).toHaveMember("code");
94
            });
95
96
            it("should have a prototype method named 'native()'", () => {
97
                expect(Klass.prototype).toHaveMethod("native");
98
            });
99
        }
100
101
        // :: PROTOTYPE VALUES
102
103
        it("should have the 'class' name in the prototype property named 'name'", () => {
104
            expect(Klass.prototype.name).toEqual(klassName);
105
        });
106
107
        it("should have a dummy default value as message", () => {
108
            expect(Klass.prototype.message).toEqual("thrown");
109
        });
110
111
        it("should have a null default value as code", () => {
112
            expect(Klass.prototype.code).toBeNull();
113
        });
114
115
        // :: CONSTRUCTOR
116
117
        it("should instantiate without parameters", () => {
118
            instanceNoParameters(Klass, testNoErrors, third);
119
            testNoErrors(() => new Klass());
120
        });
121
122
        it("should instantiate with parameters", () => {
123
            instanceParameters(Klass, testNoErrors, testNoErrors, testNoErrors, third);
124
        });
125
126
        // use the tests according to the hierarchy level
127
        if (third) {
128
            testThird(Klass);
129
        } else {
130
            test(Klass);
131
        }
132
133
    });
134
135
};
136
137
// Tests that a function doesn't throw any Error
138
function testNoErrors(fn) {
139
    expect(fn).not.toThrowError("parameter 'name' must be a 'string'");
140
    expect(fn).not.toThrowError("parameter 'message' must be a 'string'");
141
    expect(fn).not.toThrowError("parameter 'code' must be a 'number'");
142
}
143
144
// Tests instantiation of a class without parameters (undefined, null or none).
145
function instanceNoParameters(Klass, fn, third) {
146
    let arg1, arg2, arg3, test;
147
    test = (() => new Klass(arg1, arg2, arg3));
148
    for (let i = 0; i < 2; i += 1) {
149
        arg1 = (i % 2 === 0 ? undefined : null);
150
        for (let j = 0; j < 2; j += 1) {
151
            arg2 = (j % 2 === 0 ? undefined : null);
152
            if (third) {
153
                fn(test);
154
            } else {
155
                for (let e = 0; e < 2; e += 1) {
156
                    arg3 = (e % 2 === 0 ? undefined : null);
157
                    fn(test);
158
                }
159
            }
160
        }
161
    }
162
}
163
164
// Tests instantiation of a class with parameters.
165
function instanceParameters(Klass, fn1, fn2, fn3, third) {
166
    let arg1, arg2, arg3, test3, args1, args2, args3;
167
    const test1 = (() => new Klass(arg1));
168
    const test2 = (() => new Klass(arg1, arg2));
169
    if (third) {
170
        test3 = (() => null);
171
        args1 = [undefined, null, Klass.prototype.message];
172
        args2 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
173
        args3 = [];
174
    } else {
175
        test3 = (() => new Klass(arg1, arg2, arg3));
176
        args1 = [undefined, null, Klass.prototype.name];
177
        args2 = [undefined, null, Klass.prototype.message];
178
        args3 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
179
    }
180
    for (let i = 0; i < args1.length; i += 1) {
181
        arg1 = args1[i];
182
        fn1(test1);
183
        for (let j = 0; j < args2.length; j += 1) {
184
            arg2 = args2[j];
185
            fn2(test2);
186
            for (let e = 0; !third && e < args3.length; e += 1) {
187
                arg3 = args3[e];
188
                fn3(test3);
189
            }
190
        }
191
    }
192
}
193
194
// Loops for each parameter of the Klass constructor.
195
// If the iteration is even, the parameter is defined.
196
// If the iteration is odd, the parameter is null.
197
function instanceDefinedOrNull(Klass, name, message, code, fn1, fn2, fn3, third) {
198
    for (let i = 0; i < 2; i += 1) {
199
        const even1   = (i % 2 === 0);
200
        const arg1    = (even1 ? (third ? message : name) : null);
201
        const source1 = new Klass(arg1);
202
        fn1(source1, even1);
203
        for (let j = 0; j < 2; j += 1) {
204
            const even2   = (j % 2 === 0);
205
            const arg2    = (even2 ? (third ? code : message) : null);
206
            const source2 = new Klass(arg1, arg2);
207
            fn2(source2, even1, even2);
208
            for (let e = 0; !third && e < 2; e += 1) {
209
                const even3   = (e % 2 === 0);
210
                const arg3    = (even3 ? code : null);
211
                const source3 = new Klass(arg1, arg2, arg3);
212
                fn3(source3, even1, even2, even3);
213
            }
214
        }
215
    }
216
}
217
218
// Loops for each parameter of the Klass constructor using wrong types to test Error throwing.
219
function instanceThrowErrors(Klass, fn1, fn2, fn3, third) {
220
    let arg1, arg2, arg3, test33, test32, test31, test21, test22, test11, len2, len3;
221
    const len1 = noStr.length;
222
    if (third) {
223
        len2   = noNmb.length;
224
        len3   = 0;
225
        test33 = test32 = test31 = (() => null);
226
    } else {
227
        len2   = len1;
228
        len3   = noNmb.length;
229
        test33 = (() => new Klass(arg1, arg2, arg3));
230
        test32 = (() => new Klass(null, arg2, arg3));
231
        test31 = (() => new Klass(null, null, arg3));
232
    }
233
    test22 = (() => new Klass(arg1, arg2));
234
    test21 = (() => new Klass(null, arg2));
235
    test11 = (() => new Klass(arg1));
236
    for (let i = 0; i < len1; i += 1) {
237
        arg1 = noStr[i];
238
        fn1(test11);
239
        for (let j = 0; j < len2; j += 1) {
240
            arg2 = (third ? noNmb[j] : noStr[j]);
241
            fn2(test21, test22);
242
            for (let e = 0; !third && e < len3; e += 1) {
243
                arg3 = noNmb[e];
244
                fn3(test31, test32, test33);
245
            }
246
        }
247
    }
248
}
249
250
// Tests classes that are a third level subclass, meaning that they require 2 arguments (message and code).
251
function testThird(Klass) {
252
253
    // :: CONSTRUCTOR
254
255
    it("should throw an Error if 'message' or 'code' are invalid parameters", () => {
256
        instanceThrowErrors(Klass, (test11) => {
257
            expect(test11).toThrowError("parameter 'message' must be a 'string'");
258
        }, (test21, test22) => {
259
            expect(test21).toThrowError("parameter 'code' must be a 'number'");
260
            expect(test22).toThrowError("parameter 'message' must be a 'string'");
261
        }, null, true);
262
    });
263
264
    // :: MEMBER PROPERTIES
265
266
    const message = "asdf";
267
    const code    = Math.round(Math.random() * 0xFFFFFFFF);
268
269
    it("should have all correct properties once instantiated", () => {
270
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
271
            if (even) {
272
                expect(instance.name).toEqual(Klass.prototype.name);
273
                expect(instance.message).toEqual(message);
274
            } else {
275
                expect(instance.name).toEqual(Klass.prototype.name);
276
                expect(instance.message).toEqual(Klass.prototype.message);
277
            }
278
            expect(instance.code).toBeNull();
279
        }, (instance, even1, even2) => {
280
            expect(instance.name).toEqual(Klass.prototype.name);
281
            expect(instance.message).toEqual(even1 ? message : Klass.prototype.message);
282
            expect(instance.code).toEqual(even2 ? code : null);
283
        }, null, true);
284
    });
285
286
    // :: MEMBER METHODS
287
288
    it("#toString()", () => {
289
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
290
            let exp;
291
            if (even) {
292
                exp = Klass.prototype.name + ": " + message + '.';
293
            } else {
294
                exp = Klass.prototype.name + ": " + Klass.prototype.message + '.';
295
            }
296
            expect(instance.toString()).toEqual(exp);
297
        }, (instance, even1, even2) => {
298
            let exp;
299
            exp = Klass.prototype.name;
300
            exp += (even2 ? " (0x" + code.toString(16) + "):" : ':' ) + ' ';
301
            exp += (even1 ? message : Klass.prototype.message) + '.';
302
            expect(instance.toString()).toEqual(exp);
303
        }, null, true);
304
    });
305
306
    it("#native()", () => {
307
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
308
            const exp = (even ? message : Klass.prototype.message);
309
            expect(instance.native()).toEqual(new Error(exp));
310
        }, (instance, even1) => {
311
            const exp = (even1 ? message : Klass.prototype.message);
312
            expect(instance.native()).toEqual(new Error(exp));
313
        }, null, true);
314
    });
315
316
}
317
318
// Tests classes that require 3 arguments (name, message and code).
319
function test(Klass) {
320
321
    // :: CONSTRUCTOR
322
323
    it("should throw an Error if 'message' or 'code' are invalid parameters", () => {
324
        instanceThrowErrors(Klass, (test11) => {
325
            expect(test11).toThrowError("parameter 'name' must be a 'string'");
326
        }, (test21, test22) => {
327
            expect(test22).toThrowError("parameter 'name' must be a 'string'");
328
            expect(test21).toThrowError("parameter 'message' must be a 'string'");
329
        }, (test31, test32, test33) => {
330
            expect(test33).toThrowError("parameter 'name' must be a 'string'");
331
            expect(test32).toThrowError("parameter 'message' must be a 'string'");
332
            expect(test31).toThrowError("parameter 'code' must be a 'number'");
333
        }, false);
334
    });
335
336
    // :: MEMBER PROPERTIES
337
338
    const code = Math.round(Math.random() * 0xFFFFFFFF);
339
340
    it("should have all correct properties once instantiated", () => {
341
        instanceDefinedOrNull(Klass, name, message, code, (instance, even) => {
342
            if (even) {
343
                expect(instance.name).toEqual(name);
344
                expect(instance.message).toEqual(Klass.prototype.message);
345
            } else {
346
                expect(instance.name).toEqual(Klass.prototype.name);
347
                expect(instance.message).toEqual(Klass.prototype.message);
348
            }
349
            expect(instance.code).toBeNull();
350
        }, (instance, even1, even2) => {
351
            expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
352
            expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
353
            expect(instance.code).toBeNull();
354
        }, (instance, even1, even2, even3) => {
355
            expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
356
            expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
357
            expect(instance.code).toEqual(even3 ? code : null);
358
        }, false);
359
    });
360
361
    // :: MEMBER METHODS
362
363
    it("#toString()", () => {
364
        instanceDefinedOrNull(Klass, name, message, code, (instance, even) => {
365
            let exp;
366
            if (even) {
367
                exp = name + ": " + Klass.prototype.message + '.';
368
            } else {
369
                exp = Klass.prototype.name + ": " + Klass.prototype.message + '.';
370
            }
371
            expect(instance.toString()).toEqual(exp);
372
        }, (instance, even1, even2) => {
373
            let exp;
374
            exp = (even1 ? name : Klass.prototype.name) + ':';
375
            exp += ' ' + (even2 ? message : Klass.prototype.message) + '.';
376
            expect(instance.toString()).toEqual(exp);
377
        }, (instance, even1, even2, even3) => {
378
            let exp;
379
            exp = (even1 ? name : Klass.prototype.name);
380
            exp += (even3 ? " (0x" + code.toString(16) + "):" : ':') + ' ';
381
            exp += (even2 ? message : Klass.prototype.message) + '.';
382
            expect(instance.toString()).toEqual(exp);
383
        }, false);
384
    });
385
386
    it("#native()", () => {
387
        instanceDefinedOrNull(Klass, name, message, code, (instance) => {
388
            const exp = Klass.prototype.message;
389
            expect(instance.native()).toEqual(new Error(exp));
390
        }, (instance, even1, even2) => {
391
            const exp = (even2 ? message : Klass.prototype.message);
392
            expect(instance.native()).toEqual(new Error(exp));
393
        }, (instance, even1, even2) => {
394
            const exp = (even2 ? message : Klass.prototype.message);
395
            expect(instance.native()).toEqual(new Error(exp));
396
        }, false);
397
    });
398
399
}